from __future__ import annotations

import os
from contextlib import ExitStack
from dataclasses import dataclass
from pathlib import PurePosixPath, PureWindowsPath
from typing import Any, Union

import pytest

from rdflib.graph import ConjunctiveGraph, Dataset, Graph
from rdflib.term import URIRef
from test.utils import (
    COLLAPSED_BNODE,
    BNodeHandling,
    GraphHelper,
    affix_tuples,
    file_uri_to_path,
)


def check(
    file_uri: str,
    expected_windows_path: str | None,
    expected_posix_path: str | None,
) -> None:
    if expected_windows_path is not None:
        expected_windows_path_object = PureWindowsPath(expected_windows_path)
    if expected_posix_path is not None:
        expected_posix_path_object = PurePosixPath(expected_posix_path)

    if expected_windows_path is not None:
        if os.name == "nt":
            assert file_uri_to_path(file_uri) == expected_windows_path_object
        assert (
            file_uri_to_path(file_uri, PureWindowsPath) == expected_windows_path_object
        )

    if expected_posix_path is not None:
        if os.name != "nt":
            assert file_uri_to_path(file_uri) == expected_posix_path_object
        assert file_uri_to_path(file_uri, PurePosixPath) == expected_posix_path_object


@pytest.mark.parametrize(
    "file_uri,expected_windows_path,expected_posix_path",
    [
        (
            r"file:///C:/Windows/System32/Drivers/etc/hosts",
            r"C:\Windows\System32\Drivers\etc\hosts",
            r"/C:/Windows/System32/Drivers/etc/hosts",
        ),
        (
            r"file:///C%3A/Windows/System32/Drivers/etc/hosts",
            None,
            r"/C:/Windows/System32/Drivers/etc/hosts",
        ),
        (
            r"file:///C:/some%20dir/some%20file",
            r"C:\some dir\some file",
            r"/C:/some dir/some file",
        ),
        (
            r"file:///C%3A/some%20dir/some%20file",
            None,
            r"/C:/some dir/some file",
        ),
        (
            r"file:///C:/Python27/Scripts/pip.exe",
            r"C:\Python27\Scripts\pip.exe",
            r"/C:/Python27/Scripts/pip.exe",
        ),
        (
            r"file:///C:/yikes/paths%20with%20spaces.txt",
            r"C:\yikes\paths with spaces.txt",
            r"/C:/yikes/paths with spaces.txt",
        ),
        (
            r"file://localhost/c:/WINDOWS/clock.avi",
            r"c:\WINDOWS\clock.avi",
            r"/c:/WINDOWS/clock.avi",
        ),
        (r"file:///home/example/.profile", None, r"/home/example/.profile"),
        (r"file:///c|/path/to/file", r"c:\path\to\file", r"/c|/path/to/file"),
        (r"file:/c|/path/to/file", r"c:\path\to\file", r"/c|/path/to/file"),
        (r"file:c|/path/to/file", r"c:\path\to\file", r"c|/path/to/file"),
        (r"file:///c:/path/to/file", r"c:\path\to\file", r"/c:/path/to/file"),
        (r"file:/c:/path/to/file", r"c:\path\to\file", r"/c:/path/to/file"),
        (r"file:c:/path/to/file", r"c:\path\to\file", r"c:/path/to/file"),
        (r"file:/path/to/file", None, r"/path/to/file"),
        (r"file:///home/user/some%20file.txt", None, r"/home/user/some file.txt"),
        (
            r"file:///C:/some%20dir/some%20file.txt",
            r"C:\some dir\some file.txt",
            r"/C:/some dir/some file.txt",
        ),
    ],
)
def test_paths(
    file_uri: str,
    expected_windows_path: str | None,
    expected_posix_path: str | None,
) -> None:
    check(file_uri, expected_windows_path, expected_posix_path)


@dataclass
class SetsEqualTestCase:
    equal: bool
    format: str | tuple[str, str]
    bnode_handling: BNodeHandling
    lhs: str
    rhs: str

    @property
    def lhs_format(self) -> str:
        if isinstance(self.format, tuple):
            return self.format[0]
        return self.format

    @property
    def rhs_format(self) -> str:
        if isinstance(self.format, tuple):
            return self.format[1]
        return self.format


@pytest.mark.parametrize(
    "test_case",
    [
        SetsEqualTestCase(
            equal=False,
            format="turtle",
            bnode_handling=BNodeHandling.COMPARE,
            lhs="""
            @prefix eg: <example:> .
            _:a _:b _:c .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
            rhs="""
            @prefix eg: <example:> .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format="turtle",
            bnode_handling=BNodeHandling.EXCLUDE,
            lhs="""
            @prefix eg: <example:> .
            _:a _:b _:c .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
            rhs="""
            @prefix eg: <example:> .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format="turtle",
            bnode_handling=BNodeHandling.COLLAPSE,
            lhs="""
            @prefix eg: <example:> .
            _:a _:b _:c .
            _:z _:b _:c .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
            rhs=f"""
            @prefix eg: <example:> .
            <{COLLAPSED_BNODE}> <{COLLAPSED_BNODE}> <{COLLAPSED_BNODE}>.
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format="turtle",
            bnode_handling=BNodeHandling.COMPARE,
            lhs="""
            <example:o0> <example:p0> <example:s0> .
            <example:o1> <example:p1> <example:s1> .
            """,
            rhs="""
            @prefix eg: <example:> .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
        ),
        SetsEqualTestCase(
            equal=False,
            format="turtle",
            bnode_handling=BNodeHandling.COMPARE,
            lhs="""
            <example:o0> <example:p0> <example:s0> .
            <example:o1> <example:p1> <example:s1> .
            <example:o2> <example:p2> <example:s2> .
            """,
            rhs="""
            @prefix eg: <example:> .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format=("nquads", "trig"),
            bnode_handling=BNodeHandling.EXCLUDE,
            lhs="""
            <example:o0> <example:p0> <example:s0> .
            <example:o1> <example:p1> <example:s1> .
            <example:o2> <example:p2> <example:s2> .
            """,
            rhs="""
            @prefix eg: <example:> .
            eg:o0 eg:p0 eg:s0 .
            eg:o1 eg:p1 eg:s1 .
            eg:o2 eg:p2 eg:s2 .
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format=("nquads", "trig"),
            bnode_handling=BNodeHandling.COMPARE,
            lhs="""
            <example:o0> <example:p0> <example:s0> .
            <example:o1> <example:p1> <example:s1> <example:g1>.
            <example:o2> <example:p2> <example:s2> <example:g2> .
            """,
            rhs="""
            @prefix eg: <example:> .
            eg:o0 eg:p0 eg:s0 .
            eg:g1 {
                eg:o1 eg:p1 eg:s1 .
            }
            eg:g2 {
                eg:o2 eg:p2 eg:s2 .
            }
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format="n3",
            bnode_handling=BNodeHandling.COMPARE,
            lhs="""
            { <example:ss0> <example:sp0> <example:so0> } <example:p0> {}.
            """,
            rhs="""
            @prefix eg: <example:> .
            { eg:ss0 eg:sp0 eg:so0 } eg:p0 {}.
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format="n3",
            bnode_handling=BNodeHandling.COMPARE,
            lhs="""
            { <example:ss0> <example:sp0> <example:so0> } <example:p0> {}.
            """,
            rhs="""
            @prefix eg: <example:> .
            { eg:ss0 eg:sp0 eg:so0 } eg:p0 {}.
            """,
        ),
        SetsEqualTestCase(
            equal=True,
            format="n3",
            bnode_handling=BNodeHandling.COMPARE,
            lhs="""
            { { <example:sss0> <example:ssp0> <example:sso0> } <example:sp0> <example:so0> } <example:p0> {}.
            """,
            rhs="""
            @prefix eg: <example:> .
            { { eg:sss0 eg:ssp0 eg:sso0 } eg:sp0 eg:so0 } eg:p0 {}.
            """,
        ),
    ],
)
def test_assert_sets_equal(test_case: SetsEqualTestCase):
    """
    GraphHelper.sets_equals and related functions work correctly in both
    positive and negative cases.
    """
    lhs_graph: Graph = Graph().parse(data=test_case.lhs, format=test_case.lhs_format)
    rhs_graph: Graph = Graph().parse(data=test_case.rhs, format=test_case.rhs_format)

    public_id = URIRef("example:graph")
    lhs_dataset: Dataset = Dataset()
    lhs_dataset.parse(
        data=test_case.lhs, format=test_case.lhs_format, publicID=public_id
    )

    rhs_dataset: Dataset = Dataset()
    rhs_dataset.parse(
        data=test_case.rhs, format=test_case.rhs_format, publicID=public_id
    )

    assert isinstance(lhs_dataset, Dataset)
    assert isinstance(rhs_dataset, Dataset)
    graph: Graph
    cgraph: Dataset
    for graph, cgraph in ((lhs_graph, lhs_dataset), (rhs_graph, rhs_dataset)):
        GraphHelper.assert_sets_equals(graph, graph, BNodeHandling.COLLAPSE)
        GraphHelper.assert_sets_equals(cgraph, cgraph, BNodeHandling.COLLAPSE)
        GraphHelper.assert_triple_sets_equals(graph, graph, BNodeHandling.COLLAPSE)
        GraphHelper.assert_triple_sets_equals(cgraph, cgraph, BNodeHandling.COLLAPSE)
        GraphHelper.assert_quad_sets_equals(cgraph, cgraph, BNodeHandling.COLLAPSE)

    if not test_case.equal:
        with pytest.raises(AssertionError):
            GraphHelper.assert_sets_equals(
                lhs_graph, rhs_graph, test_case.bnode_handling
            )
        with pytest.raises(AssertionError):
            GraphHelper.assert_sets_equals(
                lhs_dataset, rhs_dataset, test_case.bnode_handling
            )
        with pytest.raises(AssertionError):
            GraphHelper.assert_triple_sets_equals(
                lhs_graph, rhs_graph, test_case.bnode_handling
            )
        with pytest.raises(AssertionError):
            GraphHelper.assert_triple_sets_equals(
                lhs_dataset, rhs_dataset, test_case.bnode_handling
            )
        with pytest.raises(AssertionError):
            GraphHelper.assert_quad_sets_equals(
                lhs_dataset, rhs_dataset, test_case.bnode_handling
            )
    else:
        GraphHelper.assert_sets_equals(lhs_graph, rhs_graph, test_case.bnode_handling)
        GraphHelper.assert_sets_equals(
            lhs_dataset, rhs_dataset, test_case.bnode_handling
        )
        GraphHelper.assert_triple_sets_equals(
            lhs_graph, rhs_graph, test_case.bnode_handling
        )
        GraphHelper.assert_triple_sets_equals(
            lhs_dataset, rhs_dataset, test_case.bnode_handling
        )
        GraphHelper.assert_quad_sets_equals(
            lhs_dataset, rhs_dataset, test_case.bnode_handling
        )


@pytest.mark.parametrize(
    ["tuples", "prefix", "suffix", "expected_result"],
    [
        (
            [("a",), ("b",), ("c",)],
            None,
            None,
            [("a",), ("b",), ("c",)],
        ),
        (
            [("a",), ("b",), ("c",)],
            (1, 2),
            None,
            [
                (1, 2, "a"),
                (1, 2, "b"),
                (1, 2, "c"),
            ],
        ),
        (
            [("a",), ("b",), ("c",)],
            None,
            (3, 4),
            [
                ("a", 3, 4),
                ("b", 3, 4),
                ("c", 3, 4),
            ],
        ),
    ],
)
def test_prefix_tuples(
    tuples: list[tuple[Any, ...]],
    prefix: tuple[Any, ...],
    suffix: tuple[Any, ...],
    expected_result: list[tuple[Any, ...]],
) -> None:
    assert expected_result == list(affix_tuples(prefix, tuples, suffix))


@pytest.mark.parametrize(
    ["graph_type", "format", "lhs", "rhs", "expected_result"],
    [
        (
            Dataset,
            "trig",
            """
            @prefix eg: <example:> .

            _:b0 eg:p0 eg:o0.
            eg:s1 eg:p1 eg:o1.

            eg:g0 {
                _:g0b0 eg:g0p0 eg:g0o0.
                eg:g0s1 eg:g0p1 eg:g0o1.
            }
            """,
            """
            @prefix eg: <example:> .

            _:b1 eg:p0 eg:o0.
            eg:s1 eg:p1 eg:o1.

            eg:g0 {
                _:g0b1 eg:g0p0 eg:g0o0.
                eg:g0s1 eg:g0p1 eg:g0o1.
            }
            """,
            None,
        ),
        (
            Dataset,
            "trig",
            """
            @prefix eg: <example:> .

            eg:g0 {
                _:b0 eg:g0p0 eg:g0o0.
                eg:g0s1 eg:g0p1 eg:g0o1.
            }
            """,
            """
            @prefix eg: <example:> .

            eg:g0 {
                _:b1 eg:g0p0 eg:g0o1.
                eg:g0s1 eg:g0p1 eg:g0o1.
            }
            """,
            AssertionError,
        ),
        (
            Dataset,
            "trig",
            """
            @prefix eg: <example:> .

            eg:g0 {
                _:b0 eg:g0p0 eg:g0o0.
                eg:g0s1 eg:g0p1 eg:g0o1.
            }
            """,
            """
            @prefix eg: <example:> .

            eg:g0 {
                _:b1 eg:g0p0 eg:g0o0.
                eg:g0s1 eg:g0p1 eg:g0o1.
            }

            eg:g1 {
                _:b0 eg:g1p0 eg:g1o0.
                eg:g1s1 eg:g1p1 eg:g1o1.
            }
            """,
            AssertionError,
        ),
    ],
)
def test_assert_cgraph_isomorphic(
    graph_type: type[ConjunctiveGraph],
    format: str,
    lhs: str,
    rhs: str,
    expected_result: Union[None, type[Exception]],
) -> None:
    lhs_graph = graph_type()
    lhs_graph.parse(data=lhs, format=format)
    rhs_graph = graph_type()
    rhs_graph.parse(data=rhs, format=format)
    catcher: pytest.ExceptionInfo[Exception] | None = None
    with ExitStack() as xstack:
        if expected_result is not None:
            catcher = xstack.enter_context(pytest.raises(expected_result))
        GraphHelper.assert_cgraph_isomorphic(lhs_graph, rhs_graph, exclude_bnodes=True)
    if expected_result is None:
        assert catcher is None
    else:
        assert catcher is not None
